////////////////// 自定义 shell 

/// shell 初识我们知道的事情
// 0 . 获取系统的环境变量 
// 1 . 会打印出命令行
// 2 . 会等待用户输入命名
// 3 . 会处理用户输入的命令 , 分割成小字符串 , 命令和选项 
// 4 . 处理内建命令
// 5.  父进程创建子进程 , 子进程去执行命令(本质就是程序替换)	
	

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>

//获取用户名
const char* GetUserName()
{
	char* name = getenv("USER");
	return name == NULL ? "Node" : name;
}

//获取主机名
const char* GetHostName()
{
	char* hostname = getenv("HOSTNAME");
	return hostname == NULL ? "Node" : hostname;
}

//获取当前工作目录
const char* GetPwd()
{
	char* pwd = getenv("PWD");
	return pwd == NULL ? "None" : pwd;

}

// 获取家目录
const char* GetHome()
{
	char* home = getenv("HOME");	
	return home == NULL ? "None" : home;
}




#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
//制作一个命令行
void MakeCommandLine(char com_line[] , int size)
{
	// 用 snprintf , 会写入到指定字符串中 
	// snprintf , 会指定大小写入第一个字符串参数中
	// 命令写入 ComLine 这个命令行数组中
	snprintf(com_line , size ,FORMAT , GetUserName() , GetHostName() , GetPwd());
}

//打印命令行
void PrintCommandLine()
{
	char ComLine[COMMAND_SIZE];
	MakeCommandLine(ComLine , sizeof(ComLine));		
	//命令存入了 ComLine , 直接打印
	printf("%s" , ComLine);
	fflush(stdout);
}


bool GetCommandLine(char command_line[] , int size)
{
	char* st = fgets(command_line , size , stdin);
	if(st == NULL)  return false;
	command_line[strlen(command_line) - 1] = 0; //清理 \n
	if(strlen(command_line) == 0) return false;
	return true;
}


// 全局的上一次退出码
int lastcode = 0;

////////////////////////////////////////// 命令行参数表 , 把字符串分割后放入这个表里 
#define MAXARGC 128
char *g_argv[MAXARGC]; // 
int g_argc = 0; 


///////////////////////////////////////// 环境变量表 , 存的是环境变量
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;



/// 环境变量初始化
void InitEnv()
{
	
    	extern char **environ;
	// 环境变量表初始都置为 0
	memset(g_env, 0, sizeof(g_env));
        g_envs = 0;

	// 子进程继承父进程模拟系统获取
	for(int i = 0; environ[i]; i++)
    	{
       	       // 1.1 申请空间
       	         g_env[i] = (char*)malloc(strlen(environ[i])+1);
       		 strcpy(g_env[i], environ[i]);
       		 g_envs++;
     	}
		
	 // 我们加一个环境变量 , 这个变量是加到父进程的变量表里
	 g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
    	 g_env[g_envs] = NULL;

	// 导成新的环境变量
	for(int i = 0; g_env[i]; i++)
    	{	
        	putenv(g_env[i]);
    	}

   	 environ = g_env;	
}

//分割的字符
#define DEM " "

// 命令行分析 ! "ls -a -l" - > "ls" "-a" "-l"
bool CommandLineAnalysis(char command[])
{
	g_argc = 0;
	//开始分割 ,  这个是第一次分割 , 分割后会得到 "ls"
	g_argv[g_argc++] = strtok(command , DEM);
	// strtok 从第一次以后 , 第一个参数为 空
	// 一直进行分割 , 把字符串按空格一个一个分割后放入命令行参数表中
	while(g_argv[g_argc++] = strtok(nullptr ,DEM)); 
	g_argc--;	
	return true;	
}

//Test --- 测试命令行参数表个数对不对
void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]->%s\n", i, g_argv[i]);
    }

    printf("argc: %d\n", g_argc);
}


// 内建命令 - cd
void Cd()
{	
	// cd  	
	if(g_argc == 1)	
	{
		// 默认走到家目录下
		//获取家目录	
		std::string home = GetHome();
      	 	if(home.empty()) return;
       	 	chdir(home.c_str());
	}
	// cd - , cd ~ 
	else
	{
		  std::string where = g_argv[1];
   	         // cd - / cd ~
   	         if(where == "-")
   	         {
   	             // Todu
   	         }
   	         else if(where == "~")
   	         {
   	             // Todu
   	         }
   	         else
   	         { 
	            //直接走到对应路径 
   	             chdir(where.c_str());
   	         }
	
	}
}


void Echo()
{	

	std::string str = g_argv[1];
	// echo 
	if(g_argc == 2)
	{
		 std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }	
	}
}	


// 这里就不传参数了 , 因为已经有了全局的命令行参数表了 , 就能找到 !
bool Analyze_builtin_commands()
{	
	// 命令行参数表中第一个存的就是命令 , 后面都是选项 
	std::string str = g_argv[0];		
	if(str == "cd") // cd 后面什么都不加 , 默认是到家目录下 
	{	
		Cd();
		return true;
	}
	else if(str == "echo")
	{
		Echo();
		return true;
	}
//	else if(str == "export")
//	{
//		Export();
//		return true;
//	}
//	//起别名
//	else if(str == "alias")
//	{
//		Alias();
//		return true;
//	}
	
	//走到这里证明不是内建命令 , 就让子进程去执行
	return false;
}


//执行命令 
int  ExecuteCommand()
{
	//创建子进程去程序替换执行解析后的命令
	pid_t id = fork();
	if(id == 0)
	{
		//执行
		// 因为已经存在全局的命令行参数表
		// 用 execvp
		execvp(g_argv[0] , g_argv);
		//子进程退出 , 只有 execvp 执行错误才退出 , 但一般不会错误
		exit(1);
	}			
	// father
	int wstatus = 0;
	pid_t rid = waitpid(id , &wstatus , 0);
        if(rid > 0)
	{
		// 等待成功 , 拿一下子进程的退出码
		lastcode = WEXITSTATUS(wstatus);
	}	
	return 0;
}	




int main()
{
	// 在启动 shell 的时候 , 系统会自动从环境变量配置文件中读取环境变量
	// 用继承父进程环境变量模拟系统获取 , 所以不论怎么样子进程是可以得到环境变量的
	// 所以 , 就有环境变量表存从系统配置文件中获取的环境变量
	
	// 0 . 环境变量初始化
	InitEnv();
	
		
	while(1)
	{
		//1 . 打印命令行 
		PrintCommandLine();		
 		
		//2. shell 获取用户输入 
		//   把用户输入的当字符串看待 :  "ls -a -l" , 看做字符串
		//获取用户输入的命令
		char CommandLine[COMMAND_SIZE]; // 存用户输入的命令
		if(!GetCommandLine(CommandLine , sizeof(CommandLine)))
			continue;
				
		// 3. 分析用户输入的命令
		//  因为 shell 获取时获取的是一个完整的字符串, 所以 : 要分隔字符串 , 以空格分割
		//  即 :  "ls -a -l" , 要分割成 ls  -a  -l 命令和选项 		
		CommandLineAnalysis(CommandLine);
// 		PrintArgv();			


		// 4. 会分析内建命令 - > 什么是内建命令 ? 让父进程 bash 自己去执行 , 调用函数或系统调用
		// 像 cd , echo , export 这样的命令
		// 分析内建命令  
		Analyze_builtin_commands();		
	
		//5 . 执行命令
		// shell 是一个命令行解释器 , 其执行命令是让父进程 bash 的子进程去执行命令的
		// 这里执行命令就是用的程序替换了
		ExecuteCommand();
	}			
	
	return 0;
}
